/*
This file is part of [aconnect] library. 

Author: Artem Kustikov (kustikoff[at]tut.by)


This code is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any
damages arising from the use of this code.

Permission is granted to anyone to use this code for any
purpose, including commercial applications, and to alter it and
redistribute it freely, subject to the following restrictions:

1. The origin of this code must not be misrepresented; you must
not claim that you wrote the original code. If you use this
code in a product, an acknowledgment in the product documentation
would be appreciated but is not required.

2. Altered source versions must be plainly marked as such, and
must not be misrepresented as being the original code.

3. This notice may not be removed or altered from any source
distribution.
*/


#include <cerrno>

#include "util.hpp"
#include "util.time.hpp"
#include "util.network.hpp"
#include "server.hpp"

namespace aconnect 
{

	//==================================================================//
	//	struct ClientInfo
	//==================================================================//
	string ClientInfo::getRequest (SocketStateCheck &stateCheck) const {
		return util::readFromSocket (sock, stateCheck);
	}

	void ClientInfo::writeResponse (string_constref response) const {
		util::writeToSocket (sock, response);
	}

	void ClientInfo::close (bool closeSocket, bool throwError) const throw (socket_error) {
		if (closeSocket)
			util::closeSocket (this->sock, throwError);
		
		this->sock = INVALID_SOCKET;
	}

	//==================================================================//
	//	class Server													//
	//==================================================================//
	void Server::run (Server *server) 
	{
		socket_type serverSock = server->socket();

		try 
		{
       		struct sockaddr_in clientAddr;
            socket_length_type clientAddrLen = sizeof (clientAddr);
			
			while (true)
			{
				if (server->isStopped())
					break;

				socket_type clientSock = accept (serverSock, (sockaddr* ) &clientAddr, &clientAddrLen);
				
				if (server->isStopped())
					break;

				try 
				{
				
					if (clientSock == INVALID_SOCKET)
						throw socket_error (clientSock, "Client connection accepting failed");

					// server->logDebug("Socket accepted: %d", clientSock);

					ClientInfo clientInfo;
					clientInfo.sock = clientSock;
					clientInfo.port = clientAddr.sin_port;
					clientInfo.server = server;
					util::readIpAddress (clientInfo.ip, clientAddr.sin_addr);
					
					if (!server->settings().enablePooling) {
						server->workerProc()(clientInfo);
						continue;
					}

					try {
						server->process (clientInfo);

					} catch (pool_queue_full_error) {
						// overload
						server->logWarning ("Requests queue is full - connection closed.");
						clientInfo.close(true, false);
					}

				} catch (socket_error &err) { 

					if (server->isStopped())
						break;

#if defined (__GNUC__)
					if (err.code() != EAGAIN)
#endif
					{						
						server->logError ("'socket_error' caught at connection accepting: %s", err.what());
					}

				} catch (std::exception &ex)  {

					if (server->isStopped())
						break;

					// thread starting exception, but connection is open:
					//  - server unavailable (boost::thread_resource_error can be caught)
					server->logError ("Exception caught (%s): %s", 
						typeid(ex).name(), ex.what());
					
					if (clientSock != INVALID_SOCKET && 
							server->errorProcessProc())
						server->errorProcessProc()(clientSock);

					try {
						if (clientSock != INVALID_SOCKET)
							util::closeSocket ( clientSock );
					} catch (socket_error &err) {
						server->logWarning ("Client socket closing failed: %s", err.what());
					}   
				} 
				
			}

		} catch (socket_error &err) {
			server->logError ("socket_error caught in server main thread: %s", err.what());
			// close socket
            server->stop();
		
		} catch (...)  {
			server->logError ("Unknown exception caught in main aconnect server thread procedure" );
		} 

		if (server->stopProcessProc())
			server->stopProcessProc()();
	}

	// Start server - it will process request in own thread
	void Server::start (bool inCurrentThread) throw (socket_error)
	{
		boost::mutex::scoped_lock lock (_loadMutex);
		
		assert ( _port != -1 );
		assert ( _threadPool.workerProc() );

		_isStopped = false;

		if (!inCurrentThread && NULL != _mainThread)
			throw server_started_error();
	
		_socket = util::createSocket (_settings.domain);
		logDebug ("aconnect server socket created: %d, port: %d", 
			_socket, _port);
	
		applySettings ();

		// bind server
		struct sockaddr_in local;
		util::zeroMemory (&local, sizeof( local ));

		local.sin_family = settings().domain;
		local.sin_port = htons ( port() );
		
		// INADDR_ANY should be set as 0.0.0.0
		util::writeIpAddress (local.sin_addr, _settings.ip);

		char_type buff[64] = {0,};
		int formatted = snprintf (buff, sizeof(buff), "IP address: %s, port: %d", 
			util::formatIpAddr(_settings.ip).c_str(),
			port());
		assert (formatted > 0);
		
		string socketInfo (buff, formatted);

		if ( bind( _socket, (sockaddr*) &local, sizeof(local) ) != 0 )
            throw socket_error (_socket, "Could not bind socket, " + socketInfo);
		
        if ( listen( _socket, settings().backlog ) != 0 )
            throw socket_error (_socket, "Listen to socket failed, " + socketInfo);

		// run thread
		if (inCurrentThread) {
			lock.unlock();
			Server::run (this);
		
		} else {
			
			_mainThread = new boost::thread (thread_proc_adapter<server_thread_proc, Server*>
				(Server::run, this) );
			
			boost::thread::yield ();
		}
	}
	
	void Server::stop (bool waitAllWorkers)
	{
		boost::mutex::scoped_lock lock (_loadMutex);
		if (_isStopped)
			return;

		_isStopped = true;
		bool poolStopped = false;
		
		if (waitAllWorkers) {
			try {
				boost::thread stopThread = boost::thread (thread_proc_adapter<server_thread_proc, Server*>
						(Server::stopPool, this) );
				
				boost::thread::yield ();
				
				if (_stopCondition.timed_wait(lock, aconnect::util::createTimePeriod (_settings.stopWaitTimeout)) )
					poolStopped = true;

			} catch (boost::thread_resource_error& err) {
				_logger->critical("Stopping thread creation failed, %s", err.what());
				
			} catch (std::exception& err) {
				_logger->critical("Server pool stopping failed, %s", err.what());
			}
		
		}
		
		if (!poolStopped)
			_threadPool.stop(false);
		
		try {
			if (_socket != INVALID_SOCKET) {
				util::closeSocket ( _socket );
				_socket = INVALID_SOCKET;
			}
		} catch (socket_error &err) {
			logWarning (err);
		}

		clear ();      
	}

	void Server::stopPool (Server *server) 
	{
		server->_threadPool.stop(true);
		server->_stopCondition.notify_one();
	}

	

	void Server::applySettings () 
	{
		int intValue = 0;
		
		intValue = (_settings.reuseAddr ? 1 : 0);
		if ( setsockopt( _socket, SOL_SOCKET, SO_REUSEADDR, 
			(char *) &intValue, sizeof(intValue) ) != 0 )
				throw socket_error (_socket, "Reuse address option setup failed");

		util::setSocketReadTimeout ( _socket, _settings.socketReadTimeout );
		util::setSocketWriteTimeout ( _socket, _settings.socketWriteTimeout );
	}

	//==================================================================//
    //      Default pool callbacks										//
	//==================================================================//
	void Server::checkProcessedClientState (const aconnect::ClientInfo& client)
	{
		if (!client.isClosed())
			client.close(true, false);
	}

	void Server::checkQueuedClientState (const aconnect::ClientInfo& client)
	{
		if (client.sock == INVALID_SOCKET)
			throw application_error ("Not connected client in thread pool queue");
	}
	//
	//
	//////////////////////////////////////////////////////////////////////////
}
